﻿using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using agsXMPP;
using agsXMPP.Xml;
using agsXMPP.protocol.sasl;
using agsXMPP.protocol.client;
using NabNetLib.Clapier;
using System.Configuration;



namespace NabNetLib {
    public class XmppServerConnection
    {
        private StreamParser mParser;
        private NetworkStream mNetworkStream;
        private Socket mSoc;
        private int mCurrentAuthStep = 0;
        private Lapin mLapin = null;
        private nabnetEntities mObjectContext = null;
        private object mLocker = new object();

        public Lapin Lapin
        {
            get
            {
                return mLapin;
            }
        }

        public XmppServerConnection(Socket soc,nabnetEntities objectContext)
        {
            this.mSoc = soc;
            this.mNetworkStream = new NetworkStream(soc);
            
            mObjectContext = objectContext;
        }

        public void Start()
        {
            try
            {
                this.mParser = new StreamParser();
                mParser.OnError += new ErrorHandler(mParser_OnError);
                mParser.OnStreamStart += new StreamHandler(mParser_OnStreamStart);
                mParser.OnStreamEnd += new StreamHandler(mParser_OnStreamEnd);
                mParser.OnStreamElement += new StreamHandler(mParser_OnStreamElement);
                mParser.OnStreamError += new StreamError(mParser_OnStreamError);
                HandleStream();
            }
            catch (Exception ex)
            {
                Logger.Log("Error during XmppServerConnection.Start", ex);
                throw;
            }
        }

        public void RfidMessage(string rfidSerialNumber)
        {
            //todo pass to plugin and write answer to lapin
            DebugPlayMp3();
        }

        public void RecordMessage(string wavPath)
        {
            //todo pass to plugin and write answer to lapin
            DebugPlayMp3();
        }

        public void SendPacketToRabbit(Packet.Packet packet)
        {
            try
            {
                Message m = new Message(new Jid(mLapin.serial_number + "@artiche.pck.nerim.net/idle"))
                                {
                                    Id = "NabNet" + msgNumber++,
                                    From = new Jid("net.nab.platform@artiche.pck.nerim.net/services")
                                };

                string violetContent = "<packet xmlns='violet:packet' format='1.0' ttl='604800'>" +
                                       Convert.ToBase64String(packet.GetData()) + "</packet>";

                m.InnerXml = violetContent;

                WriteToXmppSocket(m.ToString());
            }
            catch (Exception ex)
            {
                Logger.Log("Error during SendPacketToRabbit", ex);
                throw;
            }
        }

        private void HandleStream()
        {
            try
            {
                while (mSoc.IsConnected())
                {
                    byte[] buffer = new byte[1000];
                    int cpt = mNetworkStream.Read(buffer, 0, 1000);

                    string m = Encoding.UTF8.GetString(buffer);
                    m = m.Replace("\0", "");
                    //todo : log

                    mParser.Push(buffer, 0, cpt);
                }
            }
            catch (Exception ex)
            {
                Logger.Log("Error during HandleStream", ex);
                throw;
            }
        }

        private void mParser_OnStreamError(object sender, Exception ex)
        {
            Logger.Log("Stream error",ex);
            throw new NotImplementedException();
        }

        private void mParser_OnStreamElement(object sender, agsXMPP.Xml.Dom.Node e)
        {
            try
            {
                switch (mCurrentAuthStep)
                {
                    case 0:
                        {
                            #region Authentication, phase 1
                            Logger.Log(LogType.Debug, "Authentication, phase 1 : " + e);
                            if (e is agsXMPP.protocol.sasl.Auth)
                            {
                                Logger.Log(LogType.Debug,"Authenticating start");
                                Random random = new Random((int) (DateTime.Now.Ticks%Int16.MaxValue));

                                int nonce = random.Next();

                                agsXMPP.protocol.sasl.Challenge challenge = new agsXMPP.protocol.sasl.Challenge()
                                                                                {
                                                                                    TextBase64 =
                                                                                        "nonce=\"" + nonce +
                                                                                        "\",qop=\"auth\",charset=utf-8,algorithm=md5-sess"
                                                                                };

                                WriteToXmppSocket(challenge.ToString());
                                mCurrentAuthStep = 1;
                            }
                            else if (e is IQ && ((IQ) e).FirstChild.TagName == "query" &&
                                     ((IQ) e).FirstChild.Namespace == "violet:iq:register")
                            {
                                Logger.Log(LogType.Debug, "A lapin wants to register");
                                IQ iq = (IQ) e;
                                IQ rIq = new IQ(IqType.result)
                                             {
                                                 Id = iq.Id,
                                                 From = iq.To
                                             };
                                rIq.InnerXml =
                                    "<query xmlns='violet:iq:register'><instructions>Choose a username and password to register with this server</instructions><username/><password/></query>";

                                WriteToXmppSocket(rIq.ToString());

                                mCurrentAuthStep = 100;
                            }
                            else
                            {
                                Failure failure = new Failure()
                                                      {
                                                          Condition = FailureCondition.UnknownCondition
                                                      };
                                WriteToXmppSocket(failure.ToString());
                            }

                            #endregion
                        }
                        break;
                    case 1:
                        {
                            #region Authentication, phase 2
                            Logger.Log(LogType.Debug, "Authentication, phase 2 : " + e);
                            if (e is agsXMPP.protocol.sasl.Response)
                            {
                                string base64Data = e.Value;
                                if ((base64Data.Length*6)%8 != 0)
                                {
                                    int cpt = ((base64Data.Length*6)%8)/2;
                                    for (int i = 0; i < cpt; i++)
                                    {
                                        base64Data += "=";
                                    }
                                }
                                byte[] tmp = Convert.FromBase64String(base64Data);
                                string authString = Encoding.UTF8.GetString(tmp);
                                authString = authString.Replace("\0", "");

                                Regex rx =
                                    new Regex(
                                        "username=\"(?<userName>[^\"]*)\",nonce=\"(?<nonce>[^\"]*)\",cnonce=\"(?<cnonce>[^\"]*)\",nc=(?<nc>[^,]*),qop=auth,digest-uri=\"(?<digestUri>[^\"]*)\",response=(?<response>[^,]*),charset=utf-8");
                                Match match = rx.Match(authString);
                                if (match.Captures.Count > 0)
                                {
                                    string username = match.Groups["userName"].Captures[0].Value;
                                    string response = match.Groups["response"].Captures[0].Value;
                                    string nonce = match.Groups["nonce"].Captures[0].Value;
                                    string cnonce = match.Groups["cnonce"].Captures[0].Value + "\0";
                                    string nc = match.Groups["nc"].Captures[0].Value;
                                    string digestUri = match.Groups["digestUri"].Captures[0].Value;


                                    mLapin = LapinManager.GetLapin(mObjectContext, username);
                                    mLapin.State = LapinState.Booting;

                                    if (mLapin != null)
                                    {

                                        bool authByPass = false;
                                        bool.TryParse(
                                            ConfigurationManager.AppSettings[Constants.STAND_ALONE_AUTH_BYPASS_KEY],
                                            out authByPass);
                                        List<string> authByPassLapins = new List<string>();

                                        #region Check if we want to bypass auth for this bunny

                                        if (authByPass)
                                        {
                                            string tmpLapins =
                                                ConfigurationManager.AppSettings[
                                                    Constants.STAND_ALONE_AUTH_BYPASS_KEY_LAPINS];
                                            if (!string.IsNullOrEmpty(tmpLapins))
                                            {
                                                authByPassLapins.AddRange(tmpLapins.Split(','));
                                            }
                                        }

                                        if (authByPassLapins.Contains(username))
                                        {
                                            // Send success
                                            Logger.Log(LogType.Info,"Sending success instead of password verification : " + username);
                                            Success success = new Success();
                                            mParser.Reset();
                                            WriteToXmppSocket(success.ToString());

                                            mCurrentAuthStep = 3;
                                        }
                                            #endregion
                                            #region Check if password detection is activated

                                        else if (mLapin.password == "")
                                        {
                                            // Check if password detection is activated :
                                            // - password is empty
                                            // - response is decoded by patched firmware

                                            Regex decodePassword =
                                                new Regex("(?<serialNumber>[^:]*)::(?<password>[^:]*):");
                                            Match decodePasswordMatch = decodePassword.Match(response);

                                            // If password is there, store it then
                                            // authenticate the bunny (because patched firmware
                                            // can't authenticate the bunny)
                                            if (decodePasswordMatch.Captures.Count > 0)
                                            {
                                                Logger.Log(LogType.Debug,"patched auth : "+username);
                                                string passwordFound =
                                                    decodePasswordMatch.Groups["password"].Captures[0].Value;

                                                mLapin.password = passwordFound;

                                                //LogInfo("Storing new password for bunny");

                                                mObjectContext.SaveChanges();

                                                Success success = new Success();
                                                mParser.Reset();
                                                WriteToXmppSocket(success.ToString());

                                                mCurrentAuthStep = 3;
                                            }
                                            else
                                            {
                                                Logger.Log(LogType.Warn, "No password, and impossible to decode current data : disconnecting");
                                                // Bad password, send failure and restart auth
                                                Failure failure = new Failure()
                                                                      {
                                                                          Condition = FailureCondition.not_authorized
                                                                      };
                                                mCurrentAuthStep = 0;
                                                WriteToXmppSocket(failure.ToString());
                                            }
                                        }
                                            #endregion
                                            #region usual authentication

                                        else
                                        {
                                            string md5Hash = ComputeMd5Hash(username, mLapin.password, nonce, cnonce, nc,
                                                                            digestUri, "AUTHENTICATE");

                                            if (response == md5Hash.ToString())
                                            {
                                                Logger.Log(LogType.Debug, "Authentication, phase 2 : password ok " + e);
                                                // Send challenge back
                                                // <challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>...</challenge>
                                                // rspauth=...

                                                agsXMPP.protocol.sasl.Challenge challenge = new agsXMPP.protocol.sasl.
                                                    Challenge()
                                                                                                {
                                                                                                    TextBase64 =
                                                                                                        "rspauth=" +
                                                                                                        ComputeMd5Hash(
                                                                                                            username,
                                                                                                            mLapin.
                                                                                                                password,
                                                                                                            nonce,
                                                                                                            cnonce, nc,
                                                                                                            digestUri,
                                                                                                            "")
                                                                                                };

                                                WriteToXmppSocket(challenge.ToString());

                                                mCurrentAuthStep = 2;
                                            }
                                            else
                                            {
                                                Logger.Log(LogType.Info,"Bad password, send failure and restart auth");
                                                // Bad password, send failure and restart auth
                                                Failure failure = new Failure()
                                                                      {
                                                                          Condition = FailureCondition.not_authorized
                                                                      };
                                                mCurrentAuthStep = 0;
                                                WriteToXmppSocket(failure.ToString());
                                            }
                                        }

                                        #endregion
                                    }
                                    else
                                    {
                                        Logger.Log(LogType.Info,"Unknown lapin : "+username);
                                        Failure failure = new Failure()
                                                              {
                                                                  Condition = FailureCondition.not_authorized
                                                              };
                                        mCurrentAuthStep = 0;
                                        WriteToXmppSocket(failure.ToString());
                                    }
                                }
                                else
                                {
                                    Logger.Log(LogType.Warn,"BadAuth step2, disconnect");
                                    //LogError("Bad Auth Step 2, disconnect");
                                    mSoc.Disconnect(false);
                                }
                            }
                            else
                            {
                                Logger.Log(LogType.Warn,"Bad auth step2, disconnect");
                                mSoc.Disconnect(false);
                            }

                            #endregion
                        }
                        break;
                    case 2:
                        {
                            #region Authentication, phase 3
                            Logger.Log(LogType.Debug, "Authentication, phase 3 : " + e);
                            // We should receive <response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
                            if (e is agsXMPP.protocol.sasl.Response)
                            {
                                // Send success
                                Success success = new Success();

                                mCurrentAuthStep = 3;

                                mParser.Reset();

                                WriteToXmppSocket(success.ToString());

                            }
                            else
                            {
                                //LogError("Bad Auth Step 3, disconnect");
                                mSoc.Disconnect(false);
                            }

                            #endregion
                        }
                        break;
                    case 3:
                        {
                            #region lapin is authenticated

                            if (e is IQ)
                            {
                                Logger.Log(LogType.Debug, "IQ element : " + e);
                                IQ iq = (IQ) e;
                                if (iq.Bind != null)
                                {
                                    string resource = iq.Bind.Resource;
                                    IQ rIq = new IQ(IqType.result) {Id = iq.Id};
                                    rIq.Bind =
                                        new agsXMPP.protocol.iq.bind.Bind(
                                            new Jid("0019db01f416@artiche.pck.nerim.net/" + resource));
                                    mLapin.XmppResource = resource;
                                    WriteToXmppSocket(rIq.ToString());
                                }
                                else if (iq.Session != null)
                                {
                                    IQ rIq = new IQ(IqType.result)
                                                 {
                                                     Id = iq.Id,
                                                     From = iq.To,
                                                     To = iq.From
                                                 };
                                    rIq.Session = new agsXMPP.protocol.iq.session.Session();
                                    WriteToXmppSocket(rIq.ToString());
                                }
                                else if (iq.FirstChild != null && iq.FirstChild.TagName == "query")
                                {
                                    agsXMPP.Xml.Dom.Element query = iq.FirstChild;
                                    if (query.FirstChild != null && query.FirstChild.TagName == "packet")
                                    {
                                        byte[] initPacket = mLapin.GetInitPacket();
                                        IQ rIq = new IQ(IqType.result)
                                                     {
                                                         Id = iq.Id,
                                                         From = iq.To,
                                                         To = iq.From
                                                     };
                                        rIq.InnerXml =
                                            "<query xmlns='violet:iq:sources'><packet xmlns='violet:packet' format='1.0' ttl='604800'>" +
                                            Convert.ToBase64String(initPacket) + "</packet></query>";
                                        WriteToXmppSocket(rIq.ToString());
                                    }
                                }
                                else if (iq.FirstChild != null && iq.FirstChild.TagName == "unbind")
                                {
                                    agsXMPP.Xml.Dom.Element unbind = iq.FirstChild;
                                    if (unbind.FirstChild != null && unbind.FirstChild.TagName == "resource")
                                    {
                                        string resource = unbind.FirstChild.Value;
                                        if (resource == "boot")
                                        {
                                            // Boot process finished
                                            mLapin.State = LapinState.Ready;
                                        }
                                        IQ rIq = new IQ(IqType.result)
                                                     {
                                                         Id = iq.Id,
                                                     };
                                        WriteToXmppSocket(rIq.ToString());
                                    }
                                }
                                else
                                {
                                    Logger.Log(LogType.Warn,"Unknown IQ : " + iq );
                                }
                            }
                            else if (e is Presence)
                            {
                                Logger.Log(LogType.Debug,"Presence element : "+e);
                                Presence presence = (Presence) e;
                                Presence rPresence = new Presence()
                                                         {
                                                             From = presence.From,
                                                             To = presence.From,
                                                             Id = presence.Id
                                                         };

                                WriteToXmppSocket(rPresence.ToString());
                            }
                            else if (e is Message)
                            {
                                #region Manage messages from lapin (button + ears)
                                Logger.Log(LogType.Debug, "Message from lapin : " + e);

                                Message msg = (Message) e;
                                if (msg.FirstChild != null)
                                {
                                    if (msg.FirstChild.TagName == "button")
                                    {
                                        Logger.Log(LogType.Debug, "Button click : " + msg.FirstChild);
                                        agsXMPP.Xml.Dom.Element button = msg.FirstChild;
                                        if (button.FirstChild != null && button.FirstChild.TagName == "clic")
                                        {
                                            string clic = button.FirstChild.Value;
                                            //todo pass message to plugin and write answer to lapin

                                            DebugPlayMp3();
                                        }
                                    }
                                    else if (msg.FirstChild.TagName == "ears")
                                    {
                                        Logger.Log(LogType.Debug, "Ears move : " + msg.FirstChild);
                                        agsXMPP.Xml.Dom.Element ears = msg.FirstChild;
                                        agsXMPP.Xml.Dom.Element left = ears.SelectSingleElement("left");
                                        agsXMPP.Xml.Dom.Element right = ears.SelectSingleElement("right");
                                        if (left != null && right != null)
                                        {
                                            string moveLeft = left.Value;
                                            string moveRight = right.Value;
                                        }
                                    }

                                }

                                #endregion
                            }
                            else
                            {
                                Logger.Log(LogType.Warn,"Unable to handle bunny xmpp message : "+e);
                            }

                            #endregion

                        }
                        break;
                    case 100:
                        {
                            #region register a brand new lapin

                            // We should receive <iq to='xmpp.nabaztag.com' type='set' id='2'><query xmlns="violet:iq:register"><username>0019db01dbd7</username><password>208e6d83bfb2</password></query></iq>

                            if (e is IQ && ((IQ) e).Type == IqType.set)
                            {
                                IQ iq = (IQ) e;
                                Regex rx =
                                    new Regex(
                                        "<query xmlns=\"violet:iq:register\"><username>(?<username>[0-9a-f]*)</username><password>(?<password>[0-9a-f]*)</password></query>");
                                Match match = rx.Match(iq.InnerXml);
                                if (match.Captures.Count > 0)
                                {
                                    string username = match.Groups["username"].Captures[0].Value;
                                    string password = match.Groups["password"].Captures[0].Value;

                                    Logger.Log(LogType.Debug,"Registering login / pass : "+username+":"+password);

                                    //todo : decode password (XOR) and record it
                                    //QByteArray t1 = QByteArray::fromHex(v1);
                                    //QByteArray t2 = QByteArray::fromHex(v2);
                                    //for(int i = 0; i < t1.size(); i++)
                                    //{
                                    //    t1[i] = (char)t1[i] ^ (char)t2[i];
                                    //}

                                    //mLapin = LapinManager.GetLapin(mObjectContext, username);
                                    //if(mLapin!=null)
                                    //{
                                    //    mLapin.password = password;
                                    //}
                                    //mObjectContext.SaveChanges();

                                    IQ rIq = new IQ(IqType.result)
                                                 {
                                                     Id = iq.Id,
                                                     From = iq.To,
                                                     To = iq.From
                                                 };
                                    rIq.InnerXml = iq.InnerXml;
                                    mCurrentAuthStep = 0;
                                    WriteToXmppSocket(rIq.ToString());
                                } else
                                {
                                    Logger.Log(LogType.Warn,"Error during registering login password" + iq.InnerXml);
                                }
                            }
                            else
                            {
                                Logger.Log(LogType.Warn,e.ToString());
                            }

                            #endregion
                        }
                        break;
                }
            
            } catch (Exception ex)
            {
                Logger.Log("Error during processing stream element", ex);
                throw;
            }
        }

        private void DebugPlayMp3()
        {
            string message = "MU broadcast/vl/Record/melanie_20110802_211125.mp3\nPL 3\nMW\n";
            Packet.MessagePacket packet =
                new Packet.MessagePacket(message);

            Message m = new Message(new Jid(mLapin.serial_number+"@artiche.pck.nerim.net/idle"))
            {
                Id = "NabNet" + msgNumber++,
                From = new Jid("net.nab.platform@artiche.pck.nerim.net/services")
            };

            string toto = "<packet xmlns='violet:packet' format='1.0' ttl='604800'>" + Convert.ToBase64String(packet.GetData()) + "</packet>";
            m.InnerXml = toto;
            WriteToXmppSocket(m.ToString());
        }

        private int msgNumber = 0;

        private void mParser_OnStreamEnd(object sender, agsXMPP.Xml.Dom.Node e)
        {
            if (mCurrentAuthStep == 3)
            {

            }
            else
            {
                throw new NotImplementedException();
            }
        }

        private void mParser_OnStreamStart(object sender, agsXMPP.Xml.Dom.Node e)
        {
            StringBuilder answer = new StringBuilder();
            if (mCurrentAuthStep == 0)
            {
                

                answer.Append(
                    "<?xml version='1.0'?><stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='2173750751' from='artiche.pck.nerim.net' version='1.0' xml:lang='en'>");
                answer.Append(
                    "<stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>DIGEST-MD5</mechanism><mechanism>PLAIN</mechanism></mechanisms><register xmlns='http://violet.net/features/violet-register'/></stream:features>");

                
            } else
            {
                answer.Append(
                                "<?xml version='1.0'?><stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='1331400675' from='artiche.pck.nerim.net' version='1.0' xml:lang='en'>");
                answer.Append(
                    "<stream:features><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><required/></bind><unbind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></stream:features>");
                
            }
            
            WriteToXmppSocket(answer.ToString());
        }

        private void mParser_OnError(object sender, Exception ex)
        {
            throw new NotImplementedException();
        }

        private void WriteToXmppSocket(string answer)
        {
            //todo : log
            lock (mLocker)
            {
                byte[] bufferResponse = Encoding.UTF8.GetBytes(answer);
                mNetworkStream.Write(bufferResponse, 0, bufferResponse.Length);
                mNetworkStream.Flush();
            }
        }

        private string ComputeMd5Hash(string username, string password, string nonce, string cnonce, string nc, string digestUri, string mode)
        {
            MD5 md5HashAlgo = MD5.Create();

            List<byte> tmpHash = new List<byte>();
            tmpHash.AddRange(md5HashAlgo.ComputeHash(Encoding.UTF8.GetBytes(username + "::" + password)));
            tmpHash.AddRange(Encoding.UTF8.GetBytes(":" + nonce + ":" + cnonce));

            byte[] tmpBufferHash = md5HashAlgo.ComputeHash(tmpHash.ToArray());
            StringBuilder HA1 = new StringBuilder();
            for (int i = 0; i < tmpBufferHash.Length; i++)
            {
                // Affiche le Hash en hexadecimal 
                HA1.Append(tmpBufferHash[i].ToString("x2"));
            }


            tmpBufferHash = md5HashAlgo.ComputeHash(Encoding.UTF8.GetBytes(mode + ":" + digestUri));
            StringBuilder HA2 = new StringBuilder(); 
            for (int i = 0; i < tmpBufferHash.Length; i++)
            {
                // Affiche le Hash en hexadecimal 
                HA2.Append(tmpBufferHash[i].ToString("x2"));
            }

            tmpBufferHash = md5HashAlgo.ComputeHash(
                Encoding.UTF8.GetBytes(HA1 + ":" + nonce + ":" + nc + ":" + cnonce + ":auth:" + HA2)
            );
            StringBuilder finalMd5Hash = new StringBuilder();
            for (int i = 0; i < tmpBufferHash.Length; i++)
            {
                // Affiche le Hash en hexadecimal 
                finalMd5Hash.Append(tmpBufferHash[i].ToString("x2"));
            }

            return finalMd5Hash.ToString();
        }


    }

    static class SocketExtensions
    {
        public static bool IsConnected(this Socket socket)
        {
            try
            {
                return !(socket.Poll(1, SelectMode.SelectRead) && socket.Available == 0);
            }
            catch (SocketException) { return false; }
        }
    }
}
